-
Notifications
You must be signed in to change notification settings - Fork 22.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a page on CSRF #38151
base: main
Are you sure you want to change the base?
Add a page on CSRF #38151
Conversation
Preview URLs External URLs (6)URL:
(comment last updated: 2025-02-19 07:23:46) |
|
||
In the example below, the user has previously signed into their bank, and the browser has stored a session cookie for the user. The page contains a {{htmlelement("form")}} element, which enables the user to transfer funds to another person. When the user submits the form, the browser sends a {{httpmethod("POST")}} request to the server, including the form data. If the user is signed in, the request includes the user's cookie. The server validates the cookie and performs the special action — in this case, transferring money: | ||
|
||
 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, yes you are right, this ought to be fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> 2b4c134
page-type: guide | ||
--- | ||
|
||
In a cross-site request forgery (CSRF) attack, an attacker tricks the user or the browser into making an HTTP request to the target site. The request includes the user's credentials and causes the server to carry out some harmful action, thinking that the user intended it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make it clear this is actually a cross site request, not XSS where its injecting malicious code in the same-site context? This is same comment as https://github.com/mdn/content/pull/38151/files#r1957450793 -
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> 2b4c134
form.submit(); | ||
``` | ||
|
||
When the user visits the page, the browser submits the form to the bank's website. Because the user is signed into their bank, the request includes the user's real cookie, so the bank's server successfully validates the request, and transfers the funds: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A little bit of a mouthful. I've also used "If the request" - mostly because this is not the default (Lax is, usually), and the cookie isn't sent.
When the user visits the page, the browser submits the form to the bank's website. Because the user is signed into their bank, the request includes the user's real cookie, so the bank's server successfully validates the request, and transfers the funds: | |
When the user visits the page, the browser submits the form to the bank's website. If the request includes the user's sign-in cookie, the bank's server will successfully validate the request, and transfers the funds: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the causality is important here. Because the user is signed into their bank, the request (might) include the user's session cookie for the bank.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did though put "include"->"may include" in 2b4c134.
|
||
 | ||
|
||
There are other ways the attacker could issue a cross-site request forgery. For example, if the website uses a {{httpmethod("GET")}} request to carry out the action, then the attacker can avoid having to use a form at all, and can execute the attack by sending the user a link to a page that contains markup like this: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might not be worth saying here but it should be said in this doc (I'm working down, so apologies if you already covered it), but as above, GET should never be used for state changing requests for exactly this reason - too easy to hack, and you can't use CRSF tokens in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I feel like we should say this but I'm not sure where to put it exactly. I mentioned it in the bit about SameSite since it's very relevant to Lax.
exactly this reason - too easy to hack, and you can't use CRSF tokens in this case.
I struggle a bit with this. How practically is this really easier than the form method? No JS needed but so what?
And yes, at least for Django you need to avoid GET
if you want to use CSRF tokens, and we could mention it there too, but that's really just a functional point about implementing that defense, and it's already covered in the Django docs for that (https://docs.djangoproject.com/en/5.1/ref/csrf/), so it doesn't feel like an "extra defense".
Mentioning it on its own section under "defenses" seems wrong too, since it's not a defense on its own.
I'll think some more about it.
Another problem with the `SameSite` attribute is that it protects you from requests from a different {{glossary("Site", "site")}}, not a different {{glossary("Origin", "origin")}}. This is a looser protection, because (for example) `https://foo.example.org` and `https://bar.example.org` are considered the same site, although they are different origins. Effectively, if you rely on same-site protection, you have to trust all your site's subdomains. | ||
|
||
Even so, it is worth setting the `SameSite` attribute for sensitive cookies to `Strict` if you can, or `Lax` if you have to. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps a "Defense summary checklist" like that one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> 60a0284
|
||
### CSRF tokens | ||
|
||
In this defense, when the server serves a page, it embeds an unpredictable value in the page, called the CSRF token. Then when the browser sends the state-changing request to the server, it includes the CSRF token in the HTTP request. The browser checks the token value and carries out the request only if it matches. Because an attacker can't guess the token value, they can't issue a successful forgery. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth commenting on the replay-preventing aspects of this? Something like
In this defense, when the server serves a page, it embeds an unpredictable value in the page, called the CSRF token. Then when the browser sends the state-changing request to the server, it includes the CSRF token in the HTTP request. The browser checks the token value and carries out the request only if it matches. Because an attacker can't guess the token value, they can't issue a successful forgery. | |
In this defense, when the server serves a page, it embeds an unpredictable value in the page, called the CSRF token. Then when the browser sends the state-changing request to the server, it includes the CSRF token in the HTTP request. The browser checks the token value and carries out the request only if it matches. Because an attacker can't guess the token value, they can't issue a successful forgery. Even if the attacker does somehow discover a token at a later point, the request can't be replayed if the token changes every time! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> 2b4c134
|
||
In this section we'll outline two alternative defenses against CSRF and a third practice which can be used to provide defense in depth for either of the other two. | ||
|
||
- The first primary defense is to use _CSRF tokens_ embedded in the page. This method is especially appropriate if you're issuing state-changing requests from form elements, as in our example above. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps
- The first primary defense is to use _CSRF tokens_ embedded in the page. This method is especially appropriate if you're issuing state-changing requests from form elements, as in our example above. | |
- The first primary defense is to use _CSRF tokens_ embedded in the page. This method is the defacto-standard approach when issuing state-changing requests from form elements, as in our example above. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"de facto" is two words but I'm not that into Latin, so I've gone with "most common method" which I hope addresses the issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> 2b4c134
|
||
- The first primary defense is to use _CSRF tokens_ embedded in the page. This method is especially appropriate if you're issuing state-changing requests from form elements, as in our example above. | ||
|
||
- The alternative defense is to ensure that state-changing requests are not _simple requests_, which enables them to rely on the browser's built-in cross-origin request blocking. This method is appropriate if you're issuing state-changing requests from JavaScript APIs like {{domxref("Window.fetch()", "fetch()")}}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps more direct?
- The alternative defense is to ensure that state-changing requests are not _simple requests_, which enables them to rely on the browser's built-in cross-origin request blocking. This method is appropriate if you're issuing state-changing requests from JavaScript APIs like {{domxref("Window.fetch()", "fetch()")}}. | |
- The alternative defense is to ensure that state-changing requests are not CORS _simple requests_, ensuring that cross-origin requests are blocked by default. This method is appropriate if you're issuing state-changing requests from JavaScript APIs like {{domxref("Window.fetch()", "fetch()")}}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps "ensuring that" -> "which ensures that", to make it clear that the blocking is a consequence of the non-simpleness, and is not an extra thing people have to do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps, though since I dislike the double-ensure use, perhaps
- The alternative defense is to ensure that state-changing requests are not _simple requests_, which enables them to rely on the browser's built-in cross-origin request blocking. This method is appropriate if you're issuing state-changing requests from JavaScript APIs like {{domxref("Window.fetch()", "fetch()")}}. | |
- The alternative defense is to ensure that state-changing requests are not CORS _simple requests_, so that cross-origin requests are blocked by default. This method is appropriate if you're issuing state-changing requests from JavaScript APIs like {{domxref("Window.fetch()", "fetch()")}}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly accepted in 2b4c134, although I didn't add "CORS" because I want to put CORS in the back seat here. A lot of people think CORS is a security feature, and I want to make it as clear as possible that what we're talking about here is the default behavior, with no CORS.
|
||
In this defense, when the server serves a page, it embeds an unpredictable value in the page, called the CSRF token. Then when the browser sends the state-changing request to the server, it includes the CSRF token in the HTTP request. The browser checks the token value and carries out the request only if it matches. Because an attacker can't guess the token value, they can't issue a successful forgery. | ||
|
||
For form submissions, the CSRF token is usually implemented as a hidden form field. For a JavaScript API like `fetch()`, the token might be placed in a cookie or embedded in the page, and the JavaScript extracts the value and sends it as an extra header. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps expand on this a little - i.e. the server.
For form submissions, the CSRF token is usually implemented as a hidden form field. For a JavaScript API like `fetch()`, the token might be placed in a cookie or embedded in the page, and the JavaScript extracts the value and sends it as an extra header. | |
For form submissions, the CSRF token is included in a hidden form field, so that on form submission its values is automatically sent back to the server for checking. | |
When using a JavaScript API like `fetch()` to submit a state-changing request, the token might be placed in a cookie or embedded in the page: the JavaScript extracts the value and sends it as an extra header. |
" it as an extra header." - if it is always the same header (?) then that should be listed here. The other doc suggests X-CSRF-Token
but I don't know if that is in any way standard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK there is no standard here but happy to be corrected.
By default, only simple requests can be sent cross-origin. The reason for allowing simple requests cross-origin is that these are the same sorts of requests that could already be made cross-origin using a `<form>` element, as in the example above. So it is assumed that websites must already implement CSRF protection against simple requests. | ||
|
||
All other requests are by default not allowed cross-origin, so a CSRF attack would not succeed if the request is not simple. | ||
|
||
So one CSRF defense is to ensure that state-changing requests are never simple requests. This of course means that a website can't use forms to issue them, so this strategy is usually applicable for a website that uses JavaScript APIs like {{domxref("Window.fetch()", "fetch()")}} to issue state-changing requests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is all useful but a bit awkward. I'm not sure "So it is assumed that websites must already implement CSRF protection against simple requests." is quite true either - I suspect it would be a non-simple request if there wasn't prior art meaning that too much of the web would breaki.
By default, only simple requests can be sent cross-origin. The reason for allowing simple requests cross-origin is that these are the same sorts of requests that could already be made cross-origin using a `<form>` element, as in the example above. So it is assumed that websites must already implement CSRF protection against simple requests. | |
All other requests are by default not allowed cross-origin, so a CSRF attack would not succeed if the request is not simple. | |
So one CSRF defense is to ensure that state-changing requests are never simple requests. This of course means that a website can't use forms to issue them, so this strategy is usually applicable for a website that uses JavaScript APIs like {{domxref("Window.fetch()", "fetch()")}} to issue state-changing requests. | |
By default, only simple requests can be sent cross-origin, while non-simple requests are blocked cross-origin by default. | |
So one CSRF defense is to ensure that state-changing requests are never simple requests, and hence will be blocked. | |
Unfortunately `<form>` submissions, as in the example above, are simple requests. | |
Therefore this strategy is usually applicable for a website that uses JavaScript APIs like {{domxref("Window.fetch()", "fetch()")}} to issue state-changing requests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it is assumed that websites must already implement CSRF protection against simple requests." is quite true either - I suspect it would be a non-simple request if there wasn't prior art meaning that too much of the web would break.
Well yes, AFAIK the reason cross-origin form requests are allowed is historical: by the time it was obvious that there was a problem here, there were too many legacy sites doing it for it to be fixed.
But because of that that we must assume people using forms are also implementing their own CSRF - we just have to assume that, because we can't fix it in the browser. This is covered quite explicitly in the CORS docs which I link in the previous para:
The motivation is that the
<form>
element from HTML 4.0 (which predates cross-site fetch() and XMLHttpRequest) can submit simple requests to any origin, so anyone writing a server must already be protecting against cross-site request forgery (CSRF). Under this assumption, the server doesn't have to opt-in (by responding to a preflight request) to receive any request that looks like a form submission, since the threat of CSRF is no worse than that of form submission.
So I'm not that keen on "unfortunately", since it implies that this is an unhappy accident, that simple requests just happen to include form submissions. But actually AIUI, by definition really, simple requests are the ones that forms can make (we might even call them "form-compatible requests"). If forms had made different requests, then the definition of a simple request would have been correspondingly different.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I made a different edit in 2b4c134.
This is obviously useful in cases where you want to accept requests from some other origins. However, it means that if your server sends an `Access-Control-Allow-Origin` response header including the sender's origin, and an `Access-Control-Allow-Credentials` response header, then the server is vulnerable to a CSRF attack from that origin. | ||
|
||
### Defense in depth: SameSite cookies | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this. In particular the potential issues with Lax have never been explained to me before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I feel this is vague and wish I had a concrete example of how an attacker could circumvent Lax
. I just copied this bit from the spec but don't really understand it. Don't tell anyone that though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a big vague. I think only top level navigations with GET include cookies, so you'd be mostly fine if you don't do something dumb like make state-changing requests using URL params.
Here is some reading https://portswigger.net/web-security/csrf/bypassing-samesite-restrictions#bypassing-samesite-lax-restrictions-using-get-requests
There are a few cases worth highlighting there - samesite isn't sameorigin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I.e. I don't see how the popup case is a bypass or in some way an avenue for a same-site attack unless it gets to you maybe to open a subdomain - i.e. same-origin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a big vague. I think only top level navigations with GET include cookies, so you'd be mostly fine if you don't do something dumb like make state-changing requests using URL params.
Seems like it's not just GET, but is only safe methods: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-5.5. And yes, the other bit of the spec does say SameSite gives you good protection if you enforce the use of unsafe methods like POST. So perhaps we can use this as a place to say don't issue state-changing requests using unsafe methods, and here's a reason why.
And if you do only use unsafe methods, then SameSite is a reasonable defense, except for the samesite != sameorigin thing.
Here is some reading https://portswigger.net/web-security/csrf/bypassing-samesite-restrictions#bypassing-samesite-lax-restrictions-using-get-requests
Ah, that's a helpful link, yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, - safe methods are this list:
"Safe" HTTP methods include "GET", "HEAD", "OPTIONS", and "TRACE", as
defined in Section 4.2.1 of [RFC7231].
So the methods you might use for changing something is GET. There is another note in the spect that safe method implementations should be kept idempotent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed 5a6d295 to go into some more detail on SameSite issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I like it a lot. It makes some of the things that aren't all that clear in https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CSRF_prevention more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work, @wbamberg. I've read through it, and it reads well and makes sense. I won't review the language in detail, as Hamish has already done that. I can give it a final language review after you've responded to his comments if needed.
Co-authored-by: Hamish Willee <[email protected]>
Co-authored-by: Hamish Willee <[email protected]>
Co-authored-by: Hamish Willee <[email protected]>
This PR adds a page on CSRF attacks.
It's potentially a replacement for https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CSRF_prevention, and compared with that page: